﻿using Devil;
using Excel;
using LitJson;
using OfficeOpenXml;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using TableCore.Core;
using TableCore.Exceptions;
using TableCore.Plugin;

namespace TableCore
{

    public partial class GTFile : Disposable
    {
        public const int START_ROW = 0;
        public const int START_COL = 1;
        public const int COMMENT_COL = 0;

        List<Content> mContents;
        HashSet<string> mFiles;
        HashSet<DataSet> mDatas;

        ParallelDispatcher.Handler mUploadCmd;
        ParallelDispatcher.CombinedHandler mExportCmd;
        ParallelDispatcher.Handler mFinishCmd;
        ParallelDispatcher.Handler mInitCodeTemplate;


        string mWorkFolder;
        string mDataOutputFolder;
        string mCodeOutputFolder;

        bool mCleanExports;
        bool mExportData;
        bool mExportCode;
        bool mStopUpload;
        bool mSkipEmptyData;
        List<OutputFile> mOutputFiles;
       
        public string WorkFolder { get { return mWorkFolder; } }
        public bool CleanExports { get { return mCleanExports; } set { mCleanExports = value; } }
        public bool SkipEmptyData { get { return mSkipEmptyData; } set { mSkipEmptyData = value; } }

        public GTFile(string baseFolder, string outputData, string outputCode)
        {
            ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

            mWorkFolder = baseFolder;
            mDataOutputFolder = outputData;
            mCodeOutputFolder = outputCode;
            mExportData = !string.IsNullOrEmpty(mDataOutputFolder);
            mExportCode = !string.IsNullOrEmpty(mCodeOutputFolder);

            mDatas = new HashSet<DataSet>();
            mFiles = new HashSet<string>();

            mContents = new List<Content>();
            mOutputFiles = new List<OutputFile>();

            mExportCmd = new ParallelDispatcher.CombinedHandler("export");
            //obsolutePath = new Regex(@"^[c-z]:\.*", RegexOptions.IgnoreCase);
            Debug.LogFormat("Active Work Folder: {0}\n Data Folder: {1}\n Code Folder: {2}", mWorkFolder, mDataOutputFolder, mCodeOutputFolder);
        }

        public bool IsComplete()
        {
            return !(mUploadCmd.IsValid || mExportCmd.IsValid || mFinishCmd.IsValid);
        }

        // 导出数据到外部文件
        void ExportOutput(string srcFile, IExportData export, bool shouldExport)
        {
            if (export == null || export.ExportFile.Id == 0)
                return;
            var path = export.ExportFile.excelFile;
            OutputFile file = null;
            lock (_lock)
            {
                for (int i = 0; i < mOutputFiles.Count; i++)
                {
                    var f = mOutputFiles[i];
                    if (StringUtil.EqualIgnoreCase(f.ExcelFile, path))
                    {
                        file = f;
                        break;
                    }
                }
                if (file == null)
                {
                    file = new OutputFile(path, mCleanExports);
                    mOutputFiles.Add(file);
                    mUploadCmd = ParallelUtils.Schedule(file.OpenFile, false, mUploadCmd);
                    file.mOpenCmd = mUploadCmd;
                }
            }
            if (file != null)
            {
                if (file.Add(srcFile, export) && shouldExport)
                    file.MarkShouldExport();
            }
        }

        // 上传（打开）待导出的文件
        public void UploadExportFile(string file, bool useFilter)
        {
            if (string.IsNullOrEmpty(file))
                return;
            lock (_lock)
            {
                if (mStopUpload)
                    throw new Exception("已经结束文件上传，请确保在 FinishUpload 之前完成上传工作.");
                InitFilters();
                //FileAction act;
                //act.file = file;
                var pname = !useFilter || mFilters == null || mFilters.Count == 0 ? file : file.Replace('\\', '/');
                var forData = mExportData && (!useFilter || mFilters == null || FileFilter.PassDataFile(pname, mFilters));
                var forCode = mExportCode && (!useFilter || mFilters == null || FileFilter.PassCodeFile(pname, mFilters));
                if (!(forData || forCode))
                    return;
                //    throw new System.Exception(string.Format("正在导出数据，无法打开文件\"{0}\"", file));
                var path = Path.GetFullPath(file);
                if (!mFiles.Add(path))
                    return;
                var cmd = new FileCmd(this, file, forCode, forData);
                mUploadCmd = ParallelUtils.Schedule(cmd, false, mUploadCmd);
                //mUploadCmd = ParallelUtils.Execute(OpenExportFile, act, false, mUploadCmd);
            }
        }

        // 结束上传文件
        public void FinishUpload()
        {
            lock (_lock)
            {
                if (mStopUpload)
                    return;
                mStopUpload = true;
                mFinishCmd = ParallelUtils.Schedule(FinishExports, false, mUploadCmd);
            }
            // Export File
        }

        IEnumerator FinishExports()
        {
            //while (mUploadCmd.IsValid || mExportCmd.IsValid)
            //{
            //    yield return null;
            //}
            yield return mUploadCmd;
            yield return mExportCmd;

            var jobs = new ParallelDispatcher.CombinedHandler("export");
            for (int i = 0; i < mOutputFiles.Count; i++)
            {
                var job = ParallelUtils.Schedule(mOutputFiles[i].UpdateContents, false, mUploadCmd);
                jobs.Combine(job);
            }
            mUploadCmd = jobs.GetHandler();
            //mUploadCmd = ParallelUtils.ExecuteParallel<OutputFile>(UpdateOutputFile, mOutputFiles, mUploadCmd);
            //mUploadCmd = ParallelUtils.Execute(SaveOutputFiles, false, mUploadCmd);
            if (mExportData)
            {
                // export files
                for (int i = 0; i < mOutputFiles.Count; i++)
                {
                    var export = mOutputFiles[i];
                    if (export.ShouldExport)
                    {
                        var cmd = new FileCmd(this, export.ExcelFile, false, true);
                        mUploadCmd = ParallelUtils.Schedule(cmd, false, mUploadCmd);
                    }
                }
            }
            //while (mUploadCmd.IsValid || mExportCmd.IsValid)
            //{
            //    yield return null;
            //}
            yield return mUploadCmd;

            yield return mExportCmd;
            lock (_lock)
            {
                mStopUpload = false;
                foreach (var tb in mDatas)
                {
                    tb.Dispose();
                }
                mDatas.Clear();
                foreach (var f in mOutputFiles)
                {
                    f.Dispose();
                }
                mOutputFiles.Clear();
                mContents.Clear();
                mFiles.Clear();
            }
        }

        //
        void DisposeTable(DataSet table)
        {
            if (table == null)
                return;
            lock (_lock)
            {
                mDatas.Remove(table);
            }
            table.Dispose();
        }


        protected override void OnDisposing()
        {
            foreach(var tb in mDatas)
            {
                tb.Dispose();
            }
            mDatas.Clear();
            foreach (var f in mOutputFiles)
            {
                f.Dispose();
            }
            mOutputFiles.Clear();
            mContents.Clear();
            mFiles.Clear();
        }


        class FileCmd : ICommand
        {
            GTFile gtFile;
            string file;
            bool forData;
            bool forCode;

            public FileCmd(GTFile gtfile, string file, bool forCode, bool forData)
            {
                this.gtFile = gtfile;
                this.file = file;
                this.forData = forData;
                this.forCode = forCode;
            }

            string GetSpaceBefore(string str, int end)
            {
                if (end <= str.Length)
                {
                    for (int i = end - 1; i >= 0; i--)
                    {
                        var c = str[i];
                        if (c != ' ' && c != '\t')
                        {
                            return end > i + 1 ? str.Substring(i + 1, end - i - 1) : "";
                        }
                    }
                }
                return "";
            }
            string GetArgName(string function)
            {
                var n1 = function.IndexOf('(');
                var n2 = function.IndexOf(')', n1 + 1);
                return n1 > 0 && n2 > n1 ? function.Substring(n1 + 1, n2 - n1 - 1) : "arg";
            }

            public void Execute()
            {
                if (string.IsNullOrEmpty(file) || !File.Exists(file))
                    throw new FileNotFoundException("GTFile Can't open file: " + file, file);
                List<Content> tables = new List<Content>();
                using (var stream = new FileStream(file, FileMode.Open))
                {
                    IExcelDataReader reader = null;
                    try
                    {
                        if (file.EndWithIgnoreCase(".xls"))
                        {
                            reader = ExcelReaderFactory.CreateBinaryReader(stream);
                        }
                        else if (file.EndWithIgnoreCase(".xlsx"))
                        {
                            reader = ExcelReaderFactory.CreateOpenXmlReader(stream);
                        }
                        reader.IsFirstRowAsColumnNames = false;
                        while (reader.Read()) { }
                        var data = reader.AsDataSet();
                        stream.Close();
                        var sheets = data.Tables;
                        for (int i = 0; i < sheets.Count; i++) // (var table in data.Tables)
                        {
                            var table = sheets[i];
                            var cmod = new ClassModel(table.TableName);
                            cmod.GetProperties(gtFile.mValueFilters, table, START_ROW, START_COL);
                            var content = new Content(gtFile, file, data, cmod, i);
                            tables.Add(content);
                        }
                        lock (gtFile._lock)
                        {
                            gtFile.mDatas.Add(data);
                            gtFile.mContents.AddRange(tables);
                        }
                        //Debug.LogFormat("upload file: {0}", action.file);
                        ParallelDispatcher.CombinedHandler export = new ParallelDispatcher.CombinedHandler("export");
                        if (forData)
                        {
                            var job = ParallelUtils.ScheduleParallel<Content>(ExportData, tables, Factory.GetInstance().GetInitializeHandler());
                            //mExportCmd.Combine(job);
                            export.Combine(job);
                        }
                        if (forCode)
                        {
                            if (gtFile.mCodeTemplate == null)
                            {
                                gtFile.mCodeTemplate = "";
                                var file = Utils.GetRelativePath("Config/CsharpTemplate.txt");
                                if (File.Exists(file))
                                    gtFile.mInitCodeTemplate = ParallelUtils.Schedule(gtFile.InitCodeTemplate, file, false);
                            }
                            var prev = new ParallelDispatcher.CombinedHandler("preExport");
                            prev.Combine(gtFile.mInitCodeTemplate);
                            prev.Combine(Factory.GetInstance().GetInitializeHandler());
                            var job = ParallelUtils.ScheduleParallel<Content>(ExportCode, tables, prev.GetHandler());
                            //mExportCmd.Combine(job);
                            export.Combine(job);
                        }
                        var clean = ParallelUtils.Schedule(gtFile.DisposeTable, data, false, export.GetHandler());
                        gtFile.mExportCmd.Combine(clean);
                    }
                    catch (Exception e)
                    {
                        Debug.LogFormat("open \"{0}\" error.\n{1}", file, e);
                    }
                }
            }

            // parallel execution, 导出文件生成数据
            void ExportData(Content content)
            {
                var cmod = content.Cmodel;
                var errorType = StringUtil.GetBuilder();
                for (int i = 0; i < cmod.PropertyCount; i++)
                {
                    var p = cmod.GetProperty(i);
                    if (p.Ignore)
                        continue;
                    if (!p.GenType.IsValidType())
                    {
                        if (errorType.Length > 0)
                            errorType.Append(',');
                        errorType.Append(p.GenType.Name);
                    }
                }
                if (errorType.Length > 0)
                    throw new TypeNotDefinedException(content, StringUtil.ReturnRelease(errorType));
                var table = content.Data.Tables[content.TableIndex];
                var row0 = START_ROW + 3;
                int rows = GetMaxRowIndex(table) + 1;// table.Rows.Count;
                //List<JObject> datas = new List<JObject>();
                var variants = new HashSet<string>();
                for (int i = 0; i < cmod.PropertyCount; i++)
                {
                    cmod.GetProperty(i).GetVariants(variants);
                }
                // 初始化生成文件
                List<GenDataVariant> exportVariants;
                if (variants.Count > 0)
                {
                    exportVariants = new List<GenDataVariant>(variants.Count);
                    foreach (var v in variants)
                    {
                        exportVariants.Add(new GenDataVariant(cmod, v, rows));
                    }
                }
                else
                {
                    exportVariants = new List<GenDataVariant>(1)
                    {
                        new GenDataVariant(cmod, null, rows)
                    };
                }
                GenDataVariant.GetPropertiesIndex(cmod, table, START_ROW, START_COL, exportVariants);
                var folder = Utils.GetFolderName(content.File);
                var pack = Utils.CalculateRelativePath(gtFile.mWorkFolder, folder);
                var outputFolder = string.IsNullOrEmpty(pack) ? gtFile.mDataOutputFolder : Path.Combine(gtFile.mDataOutputFolder, pack);
                if (!string.IsNullOrEmpty(pack))
                {
                    pack = pack.Replace('\\', '/');
                }
                // 分析生成数据
                for (int r = row0; r < rows; r++)
                {
                    var row = table.Rows[r];
                    var array = row.ItemArray;
                    for (int k = 0; k < exportVariants.Count; k++)
                    {
                        var v = exportVariants[k];
                        var data = new GenData(content);
                        if (cmod.AutoId)
                            data.id = r - row0 + 1;
                        var first = Utils.GetString(array != null && array.Length > START_COL ? array[START_COL] : null);
                        if (string.IsNullOrEmpty(first))
                            break;

                        int n1 = 0, n2 = 0;
                        for (int p = 0; p < cmod.PropertyCount; p++)
                        {
                            var property = cmod.GetProperty(p);
                            if ((property.IsID && cmod.AutoId) || property.Ignore)
                                continue;
                            var pindex = v.GetPropertyCols(p);
                            if (pindex == -1)
                                continue;
                            var beforeFormat = property.GenType.GenFormatter as IBeforeGenFormatter;
                            if (beforeFormat != null)
                                beforeFormat.BeforeFormat(pindex, array);
                            var cell = Utils.GetString(array != null && pindex < array.Length ? array[pindex] : null);
                            if (property.IsID)
                            {
                                data.SetProperty(p, cell, Utils.GetCellName(r, pindex), "ID");
                            }
                            else
                            {
                                if (gtFile.SkipEmptyData && ValueFilter.PassValue(Utils.GetCell(table, START_ROW, pindex), gtFile.mCheckNilFilters))
                                {
                                    n1++;
                                    if (string.IsNullOrEmpty(cell))
                                        n2++;
                                }
                                var cellName = Utils.GetCellName(r, pindex);
                                var comment = string.Format("{0}/{1}.{2}[{3}]", pack, table.TableName, property.Name, data.id);
                                data.SetProperty(p, cell, cellName, comment);
                                var exp = property.GenType.ExportData(content, cell, cellName, comment);
                                gtFile.ExportOutput(pack, exp, true);
                            }
                        }

                        if(gtFile.mSkipEmptyData && (n1 == n2 || n1 == 0))
                        {
                            continue;
                        }
                        v.genDatas.Add(data);
                    }
                }
                // 导出文件
                //var folder = Utils.GetFolderName(content.File);
                //var outputFolder = Utils.CalculateRelativePath(mWorkFolder, folder);
                //outputFolder = string.IsNullOrEmpty(outputFolder) ? mDataOutputFolder : Path.Combine(mDataOutputFolder, outputFolder);
                var excel = Utils.GetFileName(content.File, false);
                for (int i = 0; i < exportVariants.Count; i++)
                {
                    var v = exportVariants[i];
                    string file;
                    if (string.IsNullOrEmpty(v.Variant))
                    {
                        file = Path.Combine(outputFolder, cmod.ClassName + ".txt");
                    }
                    else
                    {
                        file = Path.Combine(outputFolder, v.Variant, cmod.ClassName + ".txt");
                    }
                    folder = Utils.GetFolderName(file);
                    lock (gtFile._lock)
                    {
                        if (!Directory.Exists(folder))
                            Directory.CreateDirectory(folder);
                    }
                    if (v.genDatas.Count == 0)
                    {
                        Debug.LogFormat("skip file (no data): {0} ({1})", file, excel);
                        continue;
                    }
                    var dt = new JsonData(JsonType.Array);
                    int id = 0;
                    while (v.genDatas.Count > 0)
                    {
                        var gen = v.genDatas.RemoveTop();
                        if (gen.id == id && dt.Count > 0)
                            throw new DuplexIdException(id.ToString(), content.File, cmod.ClassName);
                        dt.Add(gen.GetFormatData());
                    }
                    Utils.WriteFile(file, JsonMapper.ToJson(dt));
                    Debug.LogFormat("create file: {0} ({1})", file, excel);
                }
            }

            int CalculateSerializationID(IList<ClassModel.Property> properties)
            {
                var ids = new MinHeap<int>(properties.Count, (a, b) => a < b ? -1 : (a > b ? 1 : 0));
                bool hasId = false;
                for (int i = 0; i < properties.Count; i++)
                {
                    if (!hasId)
                        hasId = properties[i].IsID;
                    ids.Add(StringUtil.ToHash(properties[i].Name));
                }
                if (!hasId)
                    ids.Add(StringUtil.ToHash("id"));
                var id = 0;
                while (ids.Count > 0)
                {
                    id = Utils.Hash(id, ids.RemoveTop());
                }
                return id;
            }

            // parallel execution, 导出文件生成的代码
            void ExportCode(Content content)
            {
                if (string.IsNullOrEmpty(gtFile.mCodeTemplate) || string.IsNullOrEmpty(gtFile.mCodeOutputFolder))
                    return;
                var cmod = content.Cmodel;
                if (!gtFile.VALID_CLASS_NAME.IsMatch(cmod.ClassName))
                {
                    Debug.LogWarningFormat("skip script(invalid name)：{0} (from file: {1})", cmod.ClassName, content.File);
                    return;
                }
                if (string.IsNullOrEmpty(gtFile.mCodeLineEnding))
                    gtFile.mCodeLineEnding = "\n";
                HashSet<string> namespaces = new HashSet<string>();
                List<ClassModel.Property> properties = new List<ClassModel.Property>();
                for (int i = 0; i < cmod.PropertyCount; i++)
                {
                    var p = cmod.GetProperty(i);
                    if (p.Ignore || p.IsID)
                        continue;
                    if (!p.GenType.IsValidType())
                        throw new TypeNotDefinedException(content, p.GenType.Name);
                    properties.Add(p);
                    for (int k = 0; k < p.GenType.NamespaceCount; k++)
                    {
                        var space = p.GenType.GetNamespace(k);
                        if (!string.IsNullOrEmpty(space))
                            namespaces.Add(space);
                    }
                }
                if (properties.Count == 0)
                {
                    Debug.LogWarningFormat("skip script(no property)：{0} (from file: {1})", cmod.ClassName, content.File);
                    return;
                }
                if (gtFile.mDefaultNamespace != null)
                {
                    for (int i = 0; i < gtFile.mDefaultNamespace.Count; i++)
                    {
                        namespaces.Remove(gtFile.mDefaultNamespace[i]);
                    }
                }
                var buf = StringUtil.GetBuilder();
                // replace name space
                string code = gtFile.mCodeTemplate;
                var match = gtFile.USING_NAMESPACE.Match(code);
                if (namespaces.Count > 0 && match != null && match.Success)
                {
                    var tab = GetSpaceBefore(code, match.Index);
                    foreach (var spa in namespaces)
                    {
                        if (buf.Length > 0)
                            buf.Append(gtFile.mCodeLineEnding).Append(tab);
                        buf.Append("using ").Append(spa).Append(";");
                    }
                    code = gtFile.USING_NAMESPACE.Replace(code, buf.ToString());
                    buf.Clear();
                }
                else
                {
                    code = gtFile.USING_NAMESPACE.Replace(code, "");
                }
                // replace name
                code = gtFile.CLASS_NAME.Replace(code, cmod.ClassName);
                // replace index
                match = gtFile.INDEX_DEFINE.Match(code);
                if (match != null && match.Success)
                {
                    if (cmod.Indexed)
                    {
                        var tab = GetSpaceBefore(code, match.Index);
                        buf.Append("public const int id_index = 0;");
                        for (int i = 0; i < properties.Count; i++)
                        {
                            var p = properties[i];
                            buf.Append(gtFile.mCodeLineEnding).Append(tab);
                            buf.Append("public const int ").Append(p.Name).Append("_index = ");
                            buf.Append((i + 1).ToString()).Append(";");
                        }
                        code = gtFile.INDEX_DEFINE.Replace(code, buf.ToString());
                        buf.Clear();
                    }
                    else
                    {
                        code = gtFile.INDEX_DEFINE.Replace(code, "// define \"INDEX\" @A3 to generate index property");
                    }
                }
                // replace serialization id
                match = gtFile.SERIALIZATION_ID.Match(code);
                if(match != null && match.Success)
                {
                    //var tab = GetSpaceBefore(code, match.Index);
                    //buf.Append(gtFile.mCodeLineEnding).Append(tab);
                    buf.Append("public override int SerializationID { get { return ").Append(CalculateSerializationID(properties)).Append("; } }");
                    code = gtFile.SERIALIZATION_ID.Replace(code, buf.ToString());
                    buf.Clear();
                }
                // replace property define
                match = gtFile.PROPERTY_DEFINE.Match(code);
                if (match != null && match.Success)
                {
                    var tab = GetSpaceBefore(code, match.Index);
                    foreach (var p in properties)
                    {
                        if (buf.Length > 0)
                            buf.Append(gtFile.mCodeLineEnding).Append(tab);
                        buf.Append("public ").Append(p.GenType.DeclareName).Append(" ").Append(p.Name);
                        buf.Append(" { get; private set; }");
                    }
                    code = gtFile.PROPERTY_DEFINE.Replace(code, buf.ToString());
                    buf.Clear();
                }
                // replace deserialize
                match = gtFile.PROPERTY_DESERIALIZE.Match(code);
                if (match != null && match.Success)
                {
                    var tab = GetSpaceBefore(code, match.Index);
                    var arg = GetArgName(match.Value);
                    foreach (var p in properties)
                    {
                        if (buf.Length > 0)
                            buf.Append(gtFile.mCodeLineEnding);
                        var input = p.GenType.JsonInput;
                        input = input.Replace("{name}", p.Name);
                        input = input.Replace("{input}", arg);
                        buf.Append(input);
                    }
                    for (int i = 1; i < buf.Length - 1; i++)
                    {
                        if (buf[i] == '\n')
                            buf.Insert(i + 1, tab);
                    }
                    code = gtFile.PROPERTY_DESERIALIZE.Replace(code, buf.ToString());
                    buf.Clear();
                }
                // replace clone
                match = gtFile.CLONE.Match(code);
                if (match != null && match.Success)
                {
                    var tab = GetSpaceBefore(code, match.Index);
                    var arg = gtFile.GetArgName(match.Value);
                    foreach (var p in properties)
                    {
                        if (buf.Length > 0)
                            buf.Append(gtFile.mCodeLineEnding).Append(tab);
                        buf.Append("this.").Append(p.Name).Append(" = ");
                        buf.Append(arg).Append(".").Append(p.Name).Append(";");
                    }
                    code = gtFile.CLONE.Replace(code, buf.ToString());
                    buf.Clear();
                }

                // save file
                var excel = Utils.GetFileName(content.File, false);
                if (!Directory.Exists(gtFile.mCodeOutputFolder))
                    Directory.CreateDirectory(gtFile.mCodeOutputFolder);
                buf.Append(cmod.ClassName).Append(".cs");
                var file = Path.Combine(gtFile.mCodeOutputFolder, buf.ToString());
                File.WriteAllText(file, code);
                StringUtil.Release(buf);
                Debug.LogFormat("create script: {0} ({1})", file, excel);
            }

        }
    }
}
